/*
 * Copyright (C) 1999 Antti Koivisto (koivisto@kde.org)
 * Copyright (C) 2004, 2005, 2006, 2007, 2008 Apple Inc. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public License
 * along with this library; see the file COPYING.LIB.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 *
 */

#include "flutter/sky/engine/platform/transforms/TransformOperations.h"

#include <algorithm>
#include "flutter/sky/engine/platform/animation/AnimationUtilities.h"
#include "flutter/sky/engine/platform/geometry/FloatBox.h"
#include "flutter/sky/engine/platform/transforms/IdentityTransformOperation.h"
#include "flutter/sky/engine/platform/transforms/InterpolatedTransformOperation.h"
#include "flutter/sky/engine/platform/transforms/RotateTransformOperation.h"

namespace blink {

TransformOperations::TransformOperations(bool makeIdentity) {
  if (makeIdentity)
    m_operations.append(IdentityTransformOperation::create());
}

bool TransformOperations::operator==(const TransformOperations& o) const {
  if (m_operations.size() != o.m_operations.size())
    return false;

  unsigned s = m_operations.size();
  for (unsigned i = 0; i < s; i++) {
    if (*m_operations[i] != *o.m_operations[i])
      return false;
  }

  return true;
}

bool TransformOperations::operationsMatch(
    const TransformOperations& other) const {
  size_t numOperations = operations().size();
  // If the sizes of the function lists don't match, the lists don't match
  if (numOperations != other.operations().size())
    return false;

  // If the types of each function are not the same, the lists don't match
  for (size_t i = 0; i < numOperations; ++i) {
    if (!operations()[i]->isSameType(*other.operations()[i]))
      return false;
  }
  return true;
}

TransformOperations TransformOperations::blendByMatchingOperations(
    const TransformOperations& from,
    const double& progress) const {
  TransformOperations result;

  unsigned fromSize = from.operations().size();
  unsigned toSize = operations().size();
  unsigned size = std::max(fromSize, toSize);
  for (unsigned i = 0; i < size; i++) {
    RefPtr<TransformOperation> fromOperation =
        (i < fromSize) ? from.operations()[i].get() : 0;
    RefPtr<TransformOperation> toOperation =
        (i < toSize) ? operations()[i].get() : 0;
    RefPtr<TransformOperation> blendedOperation =
        toOperation ? toOperation->blend(fromOperation.get(), progress)
                    : (fromOperation ? fromOperation->blend(0, progress, true)
                                     : nullptr);
    if (blendedOperation)
      result.operations().append(blendedOperation);
    else {
      RefPtr<TransformOperation> identityOperation =
          IdentityTransformOperation::create();
      if (progress > 0.5)
        result.operations().append(toOperation ? toOperation
                                               : identityOperation);
      else
        result.operations().append(fromOperation ? fromOperation
                                                 : identityOperation);
    }
  }

  return result;
}

TransformOperations TransformOperations::blendByUsingMatrixInterpolation(
    const TransformOperations& from,
    double progress) const {
  TransformOperations result;
  result.operations().append(
      InterpolatedTransformOperation::create(from, *this, progress));
  return result;
}

TransformOperations TransformOperations::blend(const TransformOperations& from,
                                               double progress) const {
  if (from == *this || (!from.size() && !size()))
    return *this;

  // If either list is empty, use blendByMatchingOperations which has special
  // logic for this case.
  if (!from.size() || !size() || from.operationsMatch(*this))
    return blendByMatchingOperations(from, progress);

  return blendByUsingMatrixInterpolation(from, progress);
}

static void findCandidatesInPlane(double px,
                                  double py,
                                  double nz,
                                  double* candidates,
                                  int* numCandidates) {
  // The angle that this point is rotated with respect to the plane nz
  double phi = atan2(px, py);

  *numCandidates = 4;
  candidates[0] = phi;  // The element at 0deg (maximum x)

  for (int i = 1; i < *numCandidates; ++i)
    candidates[i] = candidates[i - 1] + M_PI_2;  // every 90 deg
  if (nz < 0.f) {
    for (int i = 0; i < *numCandidates; ++i)
      candidates[i] *= -1;
  }
}

// This method returns the bounding box that contains the starting point,
// the ending point, and any of the extrema (in each dimension) found across
// the circle described by the arc. These are then filtered to points that
// actually reside on the arc.
static void boundingBoxForArc(const FloatPoint3D& point,
                              const RotateTransformOperation& fromTransform,
                              const RotateTransformOperation& toTransform,
                              double minProgress,
                              double maxProgress,
                              FloatBox& box) {
  double candidates[6];
  int numCandidates = 0;

  FloatPoint3D axis(fromTransform.axis());
  double fromDegrees = fromTransform.angle();
  double toDegrees = toTransform.angle();

  if (axis.dot(toTransform.axis()) < 0)
    toDegrees *= -1;

  fromDegrees = blend(fromDegrees, toTransform.angle(), minProgress);
  toDegrees = blend(toDegrees, fromTransform.angle(), 1.0 - maxProgress);
  if (fromDegrees > toDegrees)
    std::swap(fromDegrees, toDegrees);

  TransformationMatrix fromMatrix;
  TransformationMatrix toMatrix;
  fromMatrix.rotate3d(fromTransform.x(), fromTransform.y(), fromTransform.z(),
                      fromDegrees);
  toMatrix.rotate3d(fromTransform.x(), fromTransform.y(), fromTransform.z(),
                    toDegrees);

  FloatPoint3D fromPoint = fromMatrix.mapPoint(point);
  FloatPoint3D toPoint = toMatrix.mapPoint(point);

  if (box.isEmpty())
    box.setOrigin(fromPoint);
  else
    box.expandTo(fromPoint);

  box.expandTo(toPoint);

  switch (fromTransform.type()) {
    case TransformOperation::RotateX:
      findCandidatesInPlane(point.y(), point.z(), fromTransform.x(), candidates,
                            &numCandidates);
      break;
    case TransformOperation::RotateY:
      findCandidatesInPlane(point.z(), point.x(), fromTransform.y(), candidates,
                            &numCandidates);
      break;
    case TransformOperation::RotateZ:
      findCandidatesInPlane(point.x(), point.y(), fromTransform.z(), candidates,
                            &numCandidates);
      break;
    default: {
      FloatPoint3D normal = axis;
      if (normal.isZero())
        return;
      normal.normalize();
      FloatPoint3D origin;
      FloatPoint3D toPoint = point - origin;
      FloatPoint3D center = origin + normal * toPoint.dot(normal);
      FloatPoint3D v1 = point - center;
      if (v1.isZero())
        return;

      v1.normalize();
      FloatPoint3D v2 = normal.cross(v1);
      // v1 is the basis vector in the direction of the point.
      // i.e. with a rotation of 0, v1 is our +x vector.
      // v2 is a perpenticular basis vector of our plane (+y).

      // Take the parametric equation of a circle.
      // (x = r*cos(t); y = r*sin(t);
      // We can treat that as a circle on the plane v1xv2
      // From that we get the parametric equations for a circle on the
      // plane in 3d space of
      // x(t) = r*cos(t)*v1.x + r*sin(t)*v2.x + cx
      // y(t) = r*cos(t)*v1.y + r*sin(t)*v2.y + cy
      // z(t) = r*cos(t)*v1.z + r*sin(t)*v2.z + cz
      // taking the derivative of (x, y, z) and solving for 0 gives us our
      // maximum/minimum x, y, z values
      // x'(t) = r*cos(t)*v2.x - r*sin(t)*v1.x = 0
      // tan(t) = v2.x/v1.x
      // t = atan2(v2.x, v1.x) + n*M_PI;

      candidates[0] = atan2(v2.x(), v1.x());
      candidates[1] = candidates[0] + M_PI;
      candidates[2] = atan2(v2.y(), v1.y());
      candidates[3] = candidates[2] + M_PI;
      candidates[4] = atan2(v2.z(), v1.z());
      candidates[5] = candidates[4] + M_PI;
      numCandidates = 6;
    } break;
  }

  double minRadians = deg2rad(fromDegrees);
  double maxRadians = deg2rad(toDegrees);
  // Once we have the candidates, we now filter them down to ones that
  // actually live on the arc, rather than the entire circle.
  for (int i = 0; i < numCandidates; ++i) {
    double radians = candidates[i];

    while (radians < minRadians)
      radians += 2.0 * M_PI;
    while (radians > maxRadians)
      radians -= 2.0 * M_PI;
    if (radians < minRadians)
      continue;

    TransformationMatrix rotation;
    rotation.rotate3d(axis.x(), axis.y(), axis.z(), rad2deg(radians));
    box.expandTo(rotation.mapPoint(point));
  }
}

bool TransformOperations::blendedBoundsForBox(const FloatBox& box,
                                              const TransformOperations& from,
                                              const double& minProgress,
                                              const double& maxProgress,
                                              FloatBox* bounds) const {
  int fromSize = from.operations().size();
  int toSize = operations().size();
  int size = std::max(fromSize, toSize);

  *bounds = box;
  for (int i = size - 1; i >= 0; i--) {
    RefPtr<TransformOperation> fromOperation =
        (i < fromSize) ? from.operations()[i] : nullptr;
    RefPtr<TransformOperation> toOperation =
        (i < toSize) ? operations()[i] : nullptr;
    if (fromOperation && fromOperation->type() == TransformOperation::None)
      fromOperation = nullptr;

    if (toOperation && toOperation->type() == TransformOperation::None)
      toOperation = nullptr;

    TransformOperation::OperationType interpolationType =
        toOperation
            ? toOperation->type()
            : fromOperation ? fromOperation->type() : TransformOperation::None;
    if (fromOperation && toOperation &&
        !fromOperation->canBlendWith(*toOperation.get()))
      return false;

    switch (interpolationType) {
      case TransformOperation::Identity:
        bounds->expandTo(box);
        continue;
      case TransformOperation::Translate:
      case TransformOperation::TranslateX:
      case TransformOperation::TranslateY:
      case TransformOperation::TranslateZ:
      case TransformOperation::Translate3D:
      case TransformOperation::Scale:
      case TransformOperation::ScaleX:
      case TransformOperation::ScaleY:
      case TransformOperation::ScaleZ:
      case TransformOperation::Scale3D:
      case TransformOperation::Skew:
      case TransformOperation::SkewX:
      case TransformOperation::SkewY:
      case TransformOperation::Perspective: {
        RefPtr<TransformOperation> fromTransform;
        RefPtr<TransformOperation> toTransform;
        if (!toOperation) {
          fromTransform =
              fromOperation->blend(toOperation.get(), 1 - minProgress, false);
          toTransform =
              fromOperation->blend(toOperation.get(), 1 - maxProgress, false);
        } else {
          fromTransform =
              toOperation->blend(fromOperation.get(), minProgress, false);
          toTransform =
              toOperation->blend(fromOperation.get(), maxProgress, false);
        }
        if (!fromTransform || !toTransform)
          continue;
        TransformationMatrix fromMatrix;
        TransformationMatrix toMatrix;
        fromTransform->apply(fromMatrix, FloatSize());
        toTransform->apply(toMatrix, FloatSize());
        FloatBox fromBox = *bounds;
        FloatBox toBox = *bounds;
        fromMatrix.transformBox(fromBox);
        toMatrix.transformBox(toBox);
        *bounds = fromBox;
        bounds->expandTo(toBox);
        continue;
      }
      case TransformOperation::Rotate:  // This is also RotateZ
      case TransformOperation::Rotate3D:
      case TransformOperation::RotateX:
      case TransformOperation::RotateY: {
        RefPtr<RotateTransformOperation> identityRotation;
        const RotateTransformOperation* fromRotation = nullptr;
        const RotateTransformOperation* toRotation = nullptr;
        if (fromOperation) {
          fromRotation =
              static_cast<const RotateTransformOperation*>(fromOperation.get());
          if (fromRotation->axis().isZero())
            fromRotation = nullptr;
        }

        if (toOperation) {
          toRotation =
              static_cast<const RotateTransformOperation*>(toOperation.get());
          if (toRotation->axis().isZero())
            toRotation = nullptr;
        }

        double fromAngle;
        double toAngle;
        FloatPoint3D axis;
        if (!RotateTransformOperation::shareSameAxis(
                fromRotation, toRotation, &axis, &fromAngle, &toAngle)) {
          return (false);
        }

        if (!fromRotation) {
          identityRotation = RotateTransformOperation::create(
              axis.x(), axis.y(), axis.z(), 0,
              fromOperation ? fromOperation->type() : toOperation->type());
          fromRotation = identityRotation.get();
        }

        if (!toRotation) {
          if (!identityRotation)
            identityRotation = RotateTransformOperation::create(
                axis.x(), axis.y(), axis.z(), 0,
                fromOperation ? fromOperation->type() : toOperation->type());
          toRotation = identityRotation.get();
        }

        FloatBox fromBox = *bounds;
        bool first = true;
        for (size_t i = 0; i < 2; ++i) {
          for (size_t j = 0; j < 2; ++j) {
            for (size_t k = 0; k < 2; ++k) {
              FloatBox boundsForArc;
              FloatPoint3D corner(fromBox.x(), fromBox.y(), fromBox.z());
              corner += FloatPoint3D(i * fromBox.width(), j * fromBox.height(),
                                     k * fromBox.depth());
              boundingBoxForArc(corner, *fromRotation, *toRotation, minProgress,
                                maxProgress, boundsForArc);
              if (first) {
                *bounds = boundsForArc;
                first = false;
              } else {
                bounds->expandTo(boundsForArc);
              }
            }
          }
        }
      }
        continue;
      case TransformOperation::None:
        continue;
      case TransformOperation::Matrix:
      case TransformOperation::Matrix3D:
      case TransformOperation::Interpolated:
        return (false);
    }
  }

  return true;
}

TransformOperations TransformOperations::add(
    const TransformOperations& addend) const {
  TransformOperations result;
  result.m_operations = operations();
  result.m_operations.appendVector(addend.operations());
  return result;
}

}  // namespace blink
